iT邦幫忙

2

JS - 非同步機制:呼叫堆疊、巨任務&微任務、事件循環

hy_c 2026-09-16 13:41:04 ‧ 2365 瀏覽
  • 分享至 

  • xImage
  •  

考慮下方程式碼,你覺得輸出會是什麼呢?

console.log("1");

setTimeout(() => {
  console.log("2");
}, 0);

Promise.resolve()
  .then(() => {
    console.log("3");
  })
  .then(() => {
    console.log("4");
  });

console.log("5");

這五個輸出,來自四種不同的機制、形式,而JS的引擎,遵守這個優先層級法則在執行,因此這篇我們要來聊聊,關於:呼叫堆疊(Call Stack)、巨任務(Macro-task)、微任務(Microtask),以及負責協調全場的 事件循環(Event Loop)。

1. 呼叫堆疊(Call Stack)

Call Stack 是一個遵循 LIFO(後進先出,Last In, First Out) 原則的資料結構。你可以把它想像成疊盤子:你最後放上去的盤子,必須第一個拿下來。

JS 是單執行緒的,代表它一次只能做一件事。當你的程式執行一個函式時,JS 引擎就會把它「推(Push)」進 Call Stack 開始執行;如果函式裡面又呼叫了另一個函式,就會再疊上去。當函式執行完畢,就會被「彈出(Pop)」堆疊。

function third(){
  console.log("3");
}

function second(){
  third();
  console.log("2");
}

function first(){
  second();
  console.log("1");
}

first();
  • 輸出結果:3 -> 2 -> 1

2. 巨任務(Macro-task)與 微任務(Microtask)

當非同步事件發生時,任務會被丟到不同的「排隊佇列(Queue)」裡。

巨任務(Macro-task / Task)

  • 常見成員:setTimeout、setInterval、I/O 操作。
  • 特點:處理較大、較重的非同步任務。每次事件循環只會處理一個巨任務。

微任務(Microtask)

  • 常見成員:Promise.then / catch / finally、queueMicrotask
  • 特點:通常是需要「盡快處理」的小任務。它的優先級極高——只要 Call Stack 一空,微任務佇列會被全部清空,才會輪到下一個巨任務。

3. 事件循環(Event Loop)

Event Loop 的工作非常純粹但極其重要,它不斷重複做這件事(像個無限迴圈):

  1. 檢查 Call Stack:看看裡面的同步任務有沒有跑完。
  2. 如果 Call Stack 是空的:
    • 優先檢查 微任務佇列(Microtask Queue),把裡面的任務全部搬進 Call Stack 執行完畢。
    • 微任務清空後,檢查 巨任務佇列(Macro-task Queue),把裡面最舊的一個任務搬進 Call Stack 執行。
  3. 重複以上步驟。

渲染 render(rAF、樣式計算、排版、繪製)是事件循環中獨立的步驟,發生在微任務清空之後,由瀏覽器決定是否執行。

4. 解釋範例:

console.log("A: 同步任務 1");

setTimeout(() => {
  console.log("B: 巨任務 (setTimeout)");
}, 0);

Promise.resolve()
  .then(() => {
    console.log("C: 微任務 1 (Promise)");
  })
  .then(() => {
    console.log("D: 微任務 2 (Promise)");
  });

console.log("E: 同步任務 2");

逐步拆解:

  1. 同步階段:
    • 印出 A: 同步任務 1
    • 遇到 setTimeout -> 丟進巨任務佇列。
    • 遇到 Promise -> 它的第一個 .then 丟進微任務佇列。
    • 印出 E: 同步任務 2
    • 目前輸出:A、E
  2. 微任務階段(Call Stack 空了,全面清空微任務):
    • 執行微任務 1 -> 印出 C: 微任務 1 (Promise),同時產生了微任務 2,也塞進微任務佇列。
    • 執行微任務 2 -> 印出 D: 微任務 2 (Promise)。
    • 目前輸出:A、E、C、D
  3. 巨任務階段(微任務清空後,跑一個巨任務):
    • 執行 setTimeout -> 印出 B: 巨任務 (setTimeout)。
    • 最終輸出:A -> E -> C -> D -> B

練習!

考慮以下程式碼,輸出是什麼呢?

button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('Microtask 1'));
  console.log('Listener 1');
});
button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('Microtask 2'));
  console.log('Listener 2');
});
button.click();

執行過程如下:

  1. 執行 button.click(),它被推進 call stack。
  2. listener 1 執行,把 Microtask 1 排進微任務佇列,然後印出 Listener 1。
  3. listener 1 結束了,但 call stack 還不是空的,因為 button.click() 還在裡面。所以這時不會清微任務。
  4. listener 2 執行,把 Microtask 2 排進佇列,然後印出 Listener 2。
  5. button.click() 結束,整段 script 也跑完,call stack 這才清空。
  6. 清空微任務佇列,依序印出 Microtask 1、Microtask 2。

所以結果是 Listener 1、Listener 2、Microtask 1、Microtask 2,你答對了嗎~

微任務不是在「一個任務結束後」才執行,而是在「call stack 變空」的。

程式碼節錄自Jake Archibald 的演講 In The Loop。

小結

推薦/參考資源&資料:

HTML 規範的事件循環處理模型
Jake Archibald 的演講 In The Loop。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
hy_c
iT邦新手 5 級 ‧ 2026-10-09 23:32:19

勘誤(2026/10):修正 渲染應為事件循環中獨立步驟;新增 Jake Archibald 的演講 In The Loop與練習。

我要留言

立即登入留言